import { reconcile } from "./reconcile";
import { runEffects, useEffect } from "./hooks";

export const FUNCTION = 0;
export const ELEMENT = 1;
export const STR = 2;
let currentVNode = null;
let willUpdateNodes = [];
let hasUpdated = false;
class VNode {
  child = null;
  type = null;
  props = null;
  tag = null;
  return = null;
  preProps = null;
  sibling = null;

  constructor(type, props, tag, parent, sibling) {
    this.type = type;
    this.tag = tag;
    this.props = props;
    this.return = parent;
    this.sibling = sibling;
  }
}

function createNodeViaCh(ch, parent, sibling) {
  if (typeof ch === "object") {
    ch.return = parent;
    return ch;
  }

  let tag;
  if (typeof ch === "string" || typeof ch === "number") {
    tag = STR;
  } else if (typeof ch === "function") {
    tag = FUNCTION;
  }

  return new VNode(ch, null, tag, parent, sibling);
}

export default function h(type, props = null, children = []) {
  const node = new VNode();
  node.type = type;
  node.props = props;
  node.key = props?.key;

  if (typeof type === "string") {
    node.tag = ELEMENT;
  } else if (typeof type === "function") {
    node.tag = FUNCTION;
  }

  children = flattenChildren(children);
  let childArray = [];
  children.forEach((ch, idx) => {
    childArray.push(createNodeViaCh(ch, node));
    if (idx > 0) {
      childArray[idx - 1].sibling = childArray[idx];
    }
  });
  node.child = childArray;

  return node;
}

function flattenChildren(children = []) {
  return children.reduce((pre, cur) => {
    return pre.concat(cur);
  }, []);
}

// call whan render root element
export function renderDom(type, container, props = null) {
  const node = new VNode(type, props, FUNCTION, null);
  node.el = document.querySelector(container);
  node.root = true;

  currentVNode = node;
  startWorkLoop();
}

function startWorkLoop() {
  let root = currentVNode;
  while (currentVNode !== null) {
    currentVNode = workLoopSync(currentVNode);
  }
  if (root.root) {
    appendALLChild(root);
  }

  // call use effect hooks
  runEffects();
}

// start render VNode
function workLoopSync(current) {
  let next = null;
  switch (current.tag) {
    case FUNCTION:
      next = createNextVnodeFunction(current);
      break;
    case STR:
      next = createNextVnodeStr(current);
      break;
    case ELEMENT:
      next = createNextVnodeElement(current);
      break;
  }

  if (!next) {
    return hostDom(current);
  }
  return next;
}

function hostDom(current) {
  while (current) {
    current = current.return;
    if (current) {
      if (current.tag === FUNCTION && !current.root) {
        current.el = current.child[0].el;
      }

      if (current.tag === ELEMENT) {
        // append all children
        appendALLChild(current);
      }

      if (current.sibling) {
        return current.sibling;
      }
    }
  }
  return null;
}

function pushCurrent(currentNode) {
  if (willUpdateNodes.indexOf(currentNode) === -1) {
    willUpdateNodes.push(currentNode);
  }

  readyToUpdate();
}

function readyToUpdate() {
  if (hasUpdated) return;

  hasUpdated = true;
  setTimeout(() => {
    willUpdateNodes.forEach((node) => {
      currentVNode = node;
      startWorkLoop();
    });
    hasUpdated = false;
    willUpdateNodes = [];
  });
}

function createNextVnodeFunction(current) {
  let child;
  const preProps = current.props;

  if (current.renderFn) {
    child = current.renderFn();
  } else {
    const renderFn = current.type(current.props, () => {
      // currentVNode = current;
      // startWorkLoop();
      pushCurrent(current);
    });
    child = renderFn();
    current.renderFn = renderFn;
  }
  // TODO RECONCILE
  const ch = reconcile(current.child, [child]);
  current.child = ch;
  current.preProps = preProps;
  ch[0].return = current;
  return ch[0];
}

function createNextVnodeStr(current) {
  current.el = document.createTextNode(current.type);
  return current.sibling;
}

function createNextVnodeElement(current) {
  current.el = document.createElement(current.type);
  if (current.props) {
    Object.keys(current.props).forEach((key) => {
      dealEvent(key, current.props[key], current); // event listener
      dealStyle(key, current.props[key], current); // style
    });
  }
  return (current.child && current.child[0]) || current.sibling;
}

function dealEvent(key, val, current) {
  const event = /^(on)(\D.+)/.exec(key);
  if (event && event[1] && event[2]) {
    const handle = `${event[1]}Handle`;
    addListener(current.el, event[2].toLocaleLowerCase(), val);
    current[handle] = val;
  }
}

function dealStyle(key, val, current) {
  if (key === "style") {
    Object.keys(val).forEach((s) => {
      current.el.style[s] = val[s];
    });
  }
}

function addListener(dom, eventName, handler) {
  dom.addEventListener(eventName, handler, false);
}

function removeListener(dom, eventName, handler) {
  dom.removeEventListener(eventName, handler);
}

function appendALLChild(parent) {
  // append all children
  let child = parent.child;
  parent.el.innerHTML = "";
  child.forEach((ch) => {
    try {
      parent.el.appendChild(ch.el);
    } catch (error) {
      console.error(error.message);
    }
  });
}

export { useEffect };
